﻿using CivitaiCLI.Exceptions;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace CivitaiCLI.Utils
{
    public static class InternetUtil
    {
        public const string FirefoxUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0";
        public const string FirefoxAccept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
        public const string ChromeUserAgent = "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.17 Safari/537.36";
        public static string DefaultUserAgent = ChromeUserAgent;
        public static string DefaultAccept = FirefoxAccept;
        public static async Task<RequestResult<string>> SendGetRequestAsync(string url, string referer = "", string userAgent = "", CookieCollection cookies = null, string acceptHeader = "text/html", string contentType = null, System.Net.Cache.RequestCacheLevel cache = System.Net.Cache.RequestCacheLevel.NoCacheNoStore, int connectTimeout = 100000, int readTimeout = 300000)
        {
            RequestResult<string> result;
            HttpWebRequest request = WebRequest.CreateHttp(url);
            request.Method = "GET";
            request.Accept = acceptHeader;
            if (contentType != null)
            {
                request.ContentType = contentType;
            }
            request.Proxy = null;
            request.CachePolicy = new System.Net.Cache.RequestCachePolicy(cache);
            request.Timeout = connectTimeout;
            request.ReadWriteTimeout = readTimeout;
            if (string.IsNullOrWhiteSpace(userAgent)) userAgent = DefaultUserAgent;
            request.UserAgent = userAgent;
            if (!string.IsNullOrWhiteSpace(referer)) request.Referer = referer;

            request.CookieContainer = cookies.ToCookieContainer();
            //TODO: handle System.Net.WebException: 'The operation has timed out'
            using (HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync())
            {
                CookieCollection responseCookies = cookies ?? new CookieCollection();
                responseCookies.Add(response.Cookies);

                using (var responseStream = response.GetResponseStream())
                using (StreamReader reader = new StreamReader(responseStream, string.IsNullOrEmpty(response.CharacterSet) ? Encoding.Default : Encoding.GetEncoding(response.CharacterSet), true))
                {
                    result = new RequestResult<string>(await reader.ReadToEndAsync(), responseCookies);
                }
            }
            return result;
        }

        public static string FormatBytesPerSec(float bytes)
        {
            if (float.IsInfinity(bytes) || float.IsNaN(bytes))
                return "";

            return string.Format("{0}/s", FormatBytes(bytes));
        }

        private static string FormatBytes(float bytes)
        {
            if (float.IsInfinity(bytes) || float.IsNaN(bytes))
                return "";

            if (bytes < 1000) //b
                return string.Format("{0:0}b", bytes);
            else if (bytes < 1000000) //kB
                return string.Format("{0:0}kB", bytes / 1000);
            else if (bytes < 1000000000) //MB
                return string.Format("{0:0.0}MB", bytes / 1000000);
            else //GB
                return string.Format("{0:0}GB", bytes / 1000000000);
        }
    }

    public class RequestResult<T>
    {
        public T ResponseData { get; private set; }
        public CookieCollection ResponseCookies { get; private set; }
        public RequestResult(T responseData, CookieCollection responseCookies)
        {
            ResponseData = responseData;
            ResponseCookies = responseCookies;
        }
    }

    public class DownloadProgress
    {
        public string Destination;
        public float Progress;
        public long BytesDownloaded;
        public long BytesTotal;
        public float BytesPerSec;
        public DownloadProgress(float progress, long downloaded, long total, float bytesPerSec, string destination)
        {
            Progress = progress;
            BytesDownloaded = downloaded;
            BytesTotal = total;
            BytesPerSec = bytesPerSec;
            Destination = destination;
        }
    }

    public class DownloadResult
    {
        public bool Success { get; private set; } = true;
        public string Error { get; private set; } = null;
        public Exception Exception { get; private set; } = null;
        public string Destination { get; private set; }
        public bool Existed { get; private set; }

        public DownloadResult(string error, Exception ex, string destination, bool existed)
        {
            if(error != null)
            {
                Success = false;
                Error = error;
                Exception = ex;
            }
            Destination = destination;
            Existed = existed;
        }
    }

    public static class HttpClientUtil
    {
        private static readonly HttpClient HttpClient;
        static HttpClientUtil()
        {
            HttpClient = new HttpClient();
        }

        public static async Task<DownloadResult> DownloadFileWithProgress(string url, string destination, IProgress<DownloadProgress> progress, int timeoutMS = 30000, List<Tuple<string, string>> reqHeaders = null, string userAgent = "", string referer = "", int bufferSize = 32768)
        {
            string error = null;
            Exception exception = null;
            bool existed = false;
            Uri uri = new Uri(url);
            int redirectCount = 0;

            try
            {
                do
                {
                    using (var cts = new CancellationTokenSource())
                    {
                        cts.CancelAfter(timeoutMS);
                        using (HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Get, uri))
                        {
                            if (string.IsNullOrEmpty(userAgent))
                                userAgent = InternetUtil.DefaultUserAgent;
                            req.Headers.Add("User-Agent", userAgent);
                            if (!string.IsNullOrEmpty(referer))
                                req.Headers.Add("Referer", referer);

                            if (reqHeaders != null)
                            {
                                for (int i = 0; i < reqHeaders.Count; i++)
                                {
                                    req.Headers.Add(reqHeaders[i].Item1, reqHeaders[i].Item2);
                                }
                            }
                            //using (var responseTimeout = cts.Token.Register(() => HttpClient.CancelPendingRequests()))
                            using (var response = await HttpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cts.Token))
                            {
                                if (!response.IsSuccessStatusCode && (int)response.StatusCode == 308)
                                {
                                    uri = response.Headers.Location;
                                }
                                else
                                {
                                    uri = null;

                                    if (cts.IsCancellationRequested)
                                        throw new TaskCanceledException();

                                    response.EnsureSuccessStatusCode();
                                    if (response.Content?.Headers?.ContentType?.MediaType is string mediaType)
                                    {
                                        if (mediaType.Equals("image/png", StringComparison.InvariantCultureIgnoreCase))
                                        {
                                            string newDest = destination.Substring(0, destination.LastIndexOf('.'));
                                            destination = $"{newDest}.png";
                                            if (File.Exists(destination))
                                            {
                                                existed = true;
                                                throw new FileExistedException();
                                            }
                                        }
                                        else if (mediaType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase))
                                        {
                                            throw new InvalidMediaTypeException(mediaType);
                                        }
                                    }
                                    long totalBytes = response.Content.Headers.ContentLength ?? 0L;
                                    using (var fileStream = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, FileOptions.Asynchronous))
                                    using (var contentStream = await response.Content.ReadAsStreamAsync())
                                    using (var tokenRegistration = cts.Token.Register(() => contentStream.Close()))
                                    {
                                        long totalBytesRead = 0;
                                        int bytesRead;
                                        long lastBytesRead = 0L;
                                        float bytesPerSec = 0f;
                                        float progressPercentage = 0f;

                                        DateTime lastTime = DateTime.UtcNow;
                                        byte[] buffer = new byte[bufferSize];
                                        while ((bytesRead = await contentStream.ReadAsync(buffer, 0, bufferSize)) != 0)
                                        {
                                            cts.CancelAfter(timeoutMS);

                                            await fileStream.WriteAsync(buffer, 0, bytesRead);

                                            totalBytesRead += bytesRead;

                                            DateTime now = DateTime.UtcNow;
                                            TimeSpan deltaTime = now - lastTime;
                                            if (deltaTime.TotalSeconds >= 0.5)
                                            {

                                                long deltaBytes = (totalBytesRead - lastBytesRead);
                                                bytesPerSec = (float)Math.Floor(deltaBytes / deltaTime.TotalSeconds);

                                                lastBytesRead = totalBytesRead;
                                                lastTime = now;
                                            }


                                            if (totalBytes != 0)
                                                progressPercentage = (float)((double)totalBytesRead / totalBytes * 100.0);

                                            progress.Report(new DownloadProgress(progressPercentage, totalBytesRead, totalBytes, bytesPerSec, destination));
                                        }
                                    }
                                }
                            }
                        }
                    }
                } while (uri != null && redirectCount++ < 5);
            }
            catch(WebException wEx)
            {
                error = "Error: " +  wEx.Status.ToString();
            }
            catch(TaskCanceledException)
            {
                error = "Canceled (Timeout)";
            }
            catch (HttpRequestException rEx)
            {
                error = rEx.Message;
            }
            catch (FileExistedException)
            {
                error = "File Existed";
            }
            catch (InvalidMediaTypeException iME)
            {
                error = $"MediaType: {iME.MediaType} (Early Access Model?)";
            }
            catch (Exception ex)
            {
                error = "Failed";
                exception = ex;
            }

            return new DownloadResult(error, exception, destination, existed);
        }
    }
}
